Carry an approval's persistence choice through elicitation - #1976
Open
SunkenInTime wants to merge 9 commits into
Open
Carry an approval's persistence choice through elicitation#1976SunkenInTime wants to merge 9 commits into
SunkenInTime wants to merge 9 commits into
Conversation
Codex Computer Use offers `persist: ["session", "always"]` in the terms of its "Allow Computer Use to use X?" prompt and remembers the app only when the answer names one. Executor lost the offer on the way in — the terms projection kept strings only — and the choice on the way out, because every adapter rebuilt the reply from `action` and `content`. So each accept was a one-time approval and the same app prompted on every call. - `ElicitationResponse.meta.persist` carries the choice; the vocabulary is closed so no host can grant more than the prompt offered. - `approvalTerms` keeps string lists, so the offered scopes reach the host. - The MCP plugin, the app-server bridge, and the MCP host (native mode) pass `_meta` through in both directions. - The model-mode `resume` tool takes `persist`; the pause output names the offered scopes and says a bare accept is one-time. - The HTTP resume API takes `persist`, and the browser approval page offers the scopes in a select. Nothing is chosen automatically. Fixes UsefulSoftwareCo#1962
SunkenInTime
force-pushed
the
elicitation-persist-choice
branch
from
September 11, 2026 19:24
bcf29c8 to
43edcee
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1962.
What was happening
Using Computer Use through Executor, every read, click, or keystroke asked "Allow Computer Use to use App?" again — including repeated operations inside one execution. Accepting worked, but nothing was remembered.
Codex leaves the lifetime of that approval to the answer. The prompt's
_metacarriespersist: ["session", "always"], and the runtime remembers the app only when the reply's_meta.persistnames one of those scopes. A reply withactionandcontentalone is a one-time approval, so the next call prompts again.I verified this against the installed
codex app-server(0.153.4) over a direct JSON-RPC session, outside Executor: fourmcpServer/tool/calls reading the same app on one thread. Accepting with{"action":"accept","content":{}}prompted on every call. Accepting once with_meta: {"persist": "always"}prompted once, wrote the app toComputerUseAppApprovals.json, and every later call ran silently — including through Executor afterwards.Where the choice was lost
approvalTermskept only string-valued terms, so the offered["session", "always"]never reached the host (packages/plugins/mcp/src/sdk/invoke.ts).ElicitationResponsehad no field an answer could carry terms in (packages/core/sdk/src/elicitation.ts).elicitation/createhandler, the app-server bridge's#completeElicitation, and the MCP host's native-mode handler all rebuilt the reply fromactionandcontent(invoke.ts,appserver-connector.ts,packages/hosts/mcp/src/tool-server.ts).resumetool and the HTTP resume API had no way to say "always".What this changes
@executor-js/sdk—ElicitationResponsegainsmeta: ElicitationResponseMeta, a closed struct whose only field ispersist. That mirrors the request-side projection: an answer can state exactly the terms the contract names, so no host can grant something a prompt never offered.offeredPersistence(request.meta)reads the scopes a prompt offers (a string list), and is the one helper the engine and the browser page share.@executor-js/plugin-mcp—approvalTermskeeps string lists, so the offered scopes reach the host alongside the stated ones (Chrome's per-sitepersist: "always"still passes as before; objects and mixed lists still don't). Theelicitation/createreply carries_meta: { persist }when an accept chose one, and only then. The app-server bridge forwards the reply's_metadown to Codex.@executor-js/execution—ResumeResponsecarriesmeta. The paused-execution output tells the model what a bare accept means and how to say otherwise, in the same instructions it already reads:@executor-js/host-mcp— native mode sends the request's terms to the client as_metaand readspersistback from the client's_meta. The model-moderesumetool takes an optionalpersist. (execute-action-resume, the shell modal's channel, is unchanged; the shell UI offers no such choice yet.)@executor-js/api/@executor-js/react—POST /executions/:id/resumeacceptspersist. The browser approval page shows a "Remember this approval" select when a pause offers scopes, defaulting to "Just this once".Nothing is chosen automatically. Without an explicit choice from the model or the user, an accept is one-time, exactly as today.
Tests
approvalTermskeeps string lists and still drops objects and mixed lists.remembered_echo) that offers scopes; the host'smeta.persistreaches the server, and a bare accept stays one-time.interaction.instructionsand in the text._metaboth ways and invents nothing when the client states nothing; model mode passes theresumetool'spersistto the engine, and omitsmetawhen none was given.bun run lint,format:check, andtypecheckpass for the touched packages, as do theplugin-mcp,execution, andhost-mcpsuites.The browser page change is small and has no e2e scenario yet; happy to add one, or split that part out, whichever you prefer.